package com.nulldev.util.io;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;

import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.other.CharsetUtil;

public class IOUtils {

    private static final int EOF = -1;
    public static final char DIR_SEPARATOR_UNIX = '/';
    public static final char DIR_SEPARATOR_WINDOWS = '\\';
    public static final char DIR_SEPARATOR;
    public static final String LINE_SEPARATOR_UNIX = "\n";
    public static final String LINE_SEPARATOR_WINDOWS = "\r\n";
    public static final String LINE_SEPARATOR;
    private static final int DEFAULT_BUFFER_SIZE = 4096;
    private static final int SKIP_BUFFER_SIZE = 2048;
    private static char[] SKIP_CHAR_BUFFER;
    private static byte[] SKIP_BYTE_BUFFER;

    static {
        DIR_SEPARATOR = File.separatorChar;
        final StringBuilderWriter buf = new StringBuilderWriter(4);
        final PrintWriter out = new PrintWriter(buf);
        out.println();
        LINE_SEPARATOR = buf.toString();
        out.close();
    }

    public static void closeQuietly(final Reader input) {
        closeQuietly((Closeable)input);
    }

    public static void closeQuietly(final Writer output) {
        closeQuietly((Closeable)output);
    }

    public static void closeQuietly(final InputStream input) {
        closeQuietly((Closeable)input);
    }

    public static void closeQuietly(final OutputStream output) {
        closeQuietly((Closeable)output);
    }

    public static void closeQuietly(final Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        }
        catch (IOException ex) {}
    }

    public static long copyLarge(final InputStream input, final OutputStream output) throws IOException {
        return copyLarge(input, output, new byte[4096]);
    }

    public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer) throws IOException {
    	if (input == null) {
    		return 0L;
    	}
        long count = 0L;
        int n = 0;
        while (-1 != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
            count += n;
        }
        return count;
    }

    public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, final long length) throws IOException {
        return copyLarge(input, output, inputOffset, length, new byte[4096]);
    }

    public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, final long length, final byte[] buffer) throws IOException {
        if (inputOffset > 0L) {
            skipFully(input, inputOffset);
        }
        if (length == 0L) {
            return 0L;
        }
        int bytesToRead;
        final int bufferLength = bytesToRead = buffer.length;
        if (length > 0L && length < bufferLength) {
            bytesToRead = (int)length;
        }
        long totalRead;
        int read;
        for (totalRead = 0L; bytesToRead > 0 && -1 != (read = input.read(buffer, 0, bytesToRead)); bytesToRead = (int)Math.min(length - totalRead, bufferLength)) {
            output.write(buffer, 0, read);
            totalRead += read;
            if (length > 0L) {}
        }
        return totalRead;
    }

    public static void skipFully(final InputStream input, final long toSkip) throws IOException {
        if (toSkip < 0L) {
            throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip);
        }
        final long skipped = skip(input, toSkip);
        if (skipped != toSkip) {
            throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
        }
    }

    public static void skipFully(final Reader input, final long toSkip) throws IOException {
        final long skipped = skip(input, toSkip);
        if (skipped != toSkip) {
            throw new EOFException("Chars to skip: " + toSkip + " actual: " + skipped);
        }
    }

    public static long skip(final InputStream input, final long toSkip) throws IOException {
        if (toSkip < 0L) {
            throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
        }
        if (IOUtils.SKIP_BYTE_BUFFER == null) {
            IOUtils.SKIP_BYTE_BUFFER = new byte[2048];
        }
        long remain;
        long n;
        for (remain = toSkip; remain > 0L; remain -= n) {
            n = input.read(IOUtils.SKIP_BYTE_BUFFER, 0, (int)Math.min(remain, 2048L));
            if (n < 0L) {
                break;
            }
        }
        return toSkip - remain;
    }

    public static long skip(final Reader input, final long toSkip) throws IOException {
        if (toSkip < 0L) {
            throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
        }
        if (IOUtils.SKIP_CHAR_BUFFER == null) {
            IOUtils.SKIP_CHAR_BUFFER = new char[2048];
        }
        long remain;
        long n;
        for (remain = toSkip; remain > 0L; remain -= n) {
            n = input.read(IOUtils.SKIP_CHAR_BUFFER, 0, (int)Math.min(remain, 2048L));
            if (n < 0L) {
                break;
            }
        }
        return toSkip - remain;
    }

    public static int copy(final InputStream input, final OutputStream output) throws IOException {
        final long count = copyLarge(input, output);
        if (count > 2147483647L) {
            return -1;
        }
        return (int) count;
    }
    
    public static String getStringFromInputStreamS(InputStream is) {
		BufferedReader br = null;
		StringBuilder sb = new StringBuilder();

		String line;
		try {
			br = new BufferedReader(new InputStreamReader(is, CharsetUtil.UTF_8));
			while ((line = br.readLine()) != null) {
				sb.append(line);
			}
		}catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return sb.toString();

	}

    public static String getStringFromInputStream(InputStream is) throws Exception {
		BufferedReader br = null;
		StringBuilder sb = new StringBuilder();

		String line;
		try {
			br = new BufferedReader(new InputStreamReader(is, CharsetUtil.UTF_8));
			while ((line = br.readLine()) != null) {
				sb.append(line);
			}
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return sb.toString();

	}
    
    public static String getStringFromInputStreamKeepNL(InputStream is) throws Exception {
		BufferedReader br = null;
		StringBuilder sb = new StringBuilder();

		String line;
		try {
			br = new BufferedReader(new InputStreamReader(is, CharsetUtil.UTF_8));
			while ((line = br.readLine()) != null) {
				sb.append(line);
				sb.append(JVMUtil.lineSep());
			}
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return sb.toString();

	}
    
    public static String getStringFromInputStream(InputStream is, final Charset charset) {
		BufferedReader br = null;
		StringBuilder sb = new StringBuilder();

		String line;
		try {
			br = new BufferedReader(new InputStreamReader(is, charset));
			while ((line = br.readLine()) != null) {
				sb.append(line);
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return sb.toString();

	}
    
    public static String stringFromInputStream(final InputStream is) {
    	try {
    		return new String(toByteArray(is));
    	}catch (Exception e) {
    		e.printStackTrace();
    		return getStringFromInputStreamS(is);
    	}
    }
    
    protected static final Charset UTF8 = Charset.forName("UTF-8");

    public static void readFully(InputStream inp, byte[] destination) throws IOException {
        readFully(inp, destination, 0, destination.length);
    }
    
    public static void readFully(InputStream inp, byte[] destination, int offset, int length) throws IOException {
        int read = 0;
        int r;
        int av0 = 0;
        while(read < length) {
        	JVMUtil.async_println("read=" + read + ",length=" + length + ",av=" + inp.available() + ",off=" + offset);
            r = inp.read(destination, offset + read, length - read);
            if(r == -1) {
                throw new EOFException("Asked to read " + length + " bytes from " + offset + " but hit EoF at " + read);
            }
            read += length - read;
            JVMUtil.async_println("new read: " + read);
        }
    }


    public static int toInt(byte b) {
        if(b < 0)
            return b & 0xff;
        return b;
    }
    
    public static byte fromInt(int i) {
        if(i > 256) {
            throw new IllegalArgumentException("Number " + i + " too big");
        }
        if(i > 127) {
            return (byte)(i-256);
        }
        return (byte)i;
    }


    public static int readOrEOF(InputStream stream) throws IOException {
        int data = stream.read();
        if (data == -1) throw new EOFException("No data remains");
        return data;
    }


    public static int getInt2(byte[] data) {
        return getInt2(data, 0);
    }
    public static int getInt2(byte[] data, int offset) {
        int i=offset;
        int b0 = data[i++] & 0xFF;
        int b1 = data[i++] & 0xFF;
        return getInt(b0, b1);
    }

    public static long getInt3(byte[] data) {
        return getInt3(data, 0);
    }
    public static long getInt3(byte[] data, int offset) {
        int i=offset;
        int b0 = data[i++] & 0xFF;
        int b1 = data[i++] & 0xFF;
        int b2 = data[i++] & 0xFF;
        return getInt(b0, b1, b2);
    }

    public static long getInt4(byte[] data) {
        return getInt4(data, 0);
    }
    public static long getInt4(byte[] data, int offset) {
        int i=offset;
        int b0 = data[i++] & 0xFF;
        int b1 = data[i++] & 0xFF;
        int b2 = data[i++] & 0xFF;
        int b3 = data[i++] & 0xFF;
        return getInt(b0, b1, b2, b3);
    }

    public static long getInt5(byte[] data) {
        return getInt5(data, 0);
    }
    public static long getInt5(byte[] data, int offset) {
        int i=offset;
        int b0 = data[i++] & 0xFF;
        int b1 = data[i++] & 0xFF;
        int b2 = data[i++] & 0xFF;
        int b3 = data[i++] & 0xFF;
        int b4 = data[i++] & 0xFF;
        return getInt(b0, b1, b2, b3, b4);
    }

    public static long getInt8(byte[] data) {
        return getInt8(data, 0);
    }
    public static long getInt8(byte[] data, int offset) {
        int i=offset;
        int b0 = data[i++] & 0xFF;
        int b1 = data[i++] & 0xFF;
        int b2 = data[i++] & 0xFF;
        int b3 = data[i++] & 0xFF;
        int b4 = data[i++] & 0xFF;
        int b5 = data[i++] & 0xFF;
        int b6 = data[i++] & 0xFF;
        int b7 = data[i++] & 0xFF;
        return getInt(b0, b1, b2, b3, b4, b5, b6, b7);
    }

    public static int getInt(int i0, int i1) {
        return (i1 << 8) + (i0 << 0);
    }
    public static long getInt(int i0, int i1, int i2) {
        return (i2 << 16) + (i1 << 8) + (i0 << 0);
    }
    public static long getInt(int i0, int i1, int i2, int i3) {
        return (i3 << 24) + (i2 << 16) + (i1 << 8) + (i0 << 0);
    }
    public static long getInt(int i0, int i1, int i2, int i3, int i4) {
        return (i4 << 32) + (i3 << 24) + (i2 << 16) + (i1 << 8) + (i0 << 0);
    }
    public static long getInt(int i0, int i1, int i2, int i3, int i4, int i5, int i6, int i7) {
        // Special check for all 0xff, to avoid overflowing long
        if (i0 == 255 && i1 == 255 && i3 == 255 && i4 == 255 && i5 == 255 && i6 == 255 && i7 == 255) return -1l;
        // Otherwise normal convert
        return (long)(i7 << 56) + (long)(i6 << 48) +
                (long)(i5 << 40) + (long)(i4 << 32) +
                (i3 << 24) + (i2 << 16) + (i1 << 8) + (i0 << 0);
    }


    public static int getInt2BE(byte[] data) {
        return getInt2BE(data, 0);
    }
    public static int getInt2BE(byte[] data, int offset) {
        int i=offset;
        int b0 = data[i++] & 0xFF;
        int b1 = data[i++] & 0xFF;
        return getIntBE(b0, b1);
    }

    public static long getInt3BE(byte[] data) {
        return getInt3BE(data, 0);
    }
    public static long getInt3BE(byte[] data, int offset) {
        int i=offset;
        int b0 = data[i++] & 0xFF;
        int b1 = data[i++] & 0xFF;
        int b2 = data[i++] & 0xFF;
        return getIntBE(b0, b1, b2);
    }

    public static long getInt4BE(byte[] data) {
        return getInt4BE(data, 0);
    }
    public static long getInt4BE(byte[] data, int offset) {
        int i=offset;
        int b0 = data[i++] & 0xFF;
        int b1 = data[i++] & 0xFF;
        int b2 = data[i++] & 0xFF;
        int b3 = data[i++] & 0xFF;
        return getIntBE(b0, b1, b2, b3);
    }

    public static int getIntBE(int i0, int i1) {
        return (i0 << 8) + (i1 << 0);
    }
    public static long getIntBE(int i0, int i1, int i2) {
        return (i0 << 16) + (i1 << 8) + (i2 << 0);
    }
    public static long getIntBE(int i0, int i1, int i2, int i3) {
        return (i0 << 24) + (i1 << 16) + (i2 << 8) + (i3 << 0);
    }


    public static void writeInt2(OutputStream out, int v) throws IOException {
        byte[] b2 = new byte[2];
        putInt2(b2, 0, v);
        out.write(b2);
    }
    public static void putInt2(byte[] data, int offset, int v) {
        int i = offset;
        data[i++] = (byte)((v >>>  0) & 0xFF);
        data[i++] = (byte)((v >>>  8) & 0xFF);
    }

    public static void writeInt3(OutputStream out, long v) throws IOException {
        byte[] b3 = new byte[3];
        putInt3(b3, 0, v);
        out.write(b3);
    }
    public static void putInt3(byte[] data, int offset, long v) {
        int i = offset;
        data[i++] = (byte)((v >>>  0) & 0xFF);
        data[i++] = (byte)((v >>>  8) & 0xFF);
        data[i++] = (byte)((v >>> 16) & 0xFF);
    }

    public static void writeInt4(OutputStream out, long v) throws IOException {
        byte[] b4 = new byte[4];
        putInt4(b4, 0, v);
        out.write(b4);
    }
    public static void putInt4(byte[] data, int offset, long v) {
        int i = offset;
        data[i++] = (byte)((v >>>  0) & 0xFF);
        data[i++] = (byte)((v >>>  8) & 0xFF);
        data[i++] = (byte)((v >>> 16) & 0xFF);
        data[i++] = (byte)((v >>> 24) & 0xFF);
    }

    public static void writeInt5(OutputStream out, long v) throws IOException {
        byte[] b5 = new byte[5];
        putInt5(b5, 0, v);
        out.write(b5);
    }
    public static void putInt5(byte[] data, int offset, long v) {
        int i = offset;
        data[i++] = (byte)((v >>>  0) & 0xFF);
        data[i++] = (byte)((v >>>  8) & 0xFF);
        data[i++] = (byte)((v >>> 16) & 0xFF);
        data[i++] = (byte)((v >>> 24) & 0xFF);
        data[i++] = (byte)((v >>> 32) & 0xFF);
    }

    public static void writeInt8(OutputStream out, long v) throws IOException {
        byte[] b8 = new byte[8];
        putInt8(b8, 0, v);
        out.write(b8);
    }
    public static void putInt8(byte[] data, int offset, long v) {
        int i = offset;
        data[i++] = (byte)((v >>>  0) & 0xFF);
        data[i++] = (byte)((v >>>  8) & 0xFF);
        data[i++] = (byte)((v >>> 16) & 0xFF);
        data[i++] = (byte)((v >>> 24) & 0xFF);
        data[i++] = (byte)((v >>> 32) & 0xFF);
        data[i++] = (byte)((v >>> 40) & 0xFF);
        data[i++] = (byte)((v >>> 48) & 0xFF);
        data[i++] = (byte)((v >>> 56) & 0xFF);
    }


    public static void writeInt2BE(OutputStream out, int v) throws IOException {
        byte[] b2 = new byte[2];
        putInt2BE(b2, 0, v);
        out.write(b2);
    }
    public static void putInt2BE(byte[] data, int offset, int v) {
        int i = offset;
        data[i+1] = (byte)((v >>>  0) & 0xFF);
        data[i+0] = (byte)((v >>>  8) & 0xFF);
    }

    public static void writeInt3BE(OutputStream out, long v) throws IOException {
        byte[] b3 = new byte[3];
        putInt3BE(b3, 0, v);
        out.write(b3);
    }
    public static void putInt3BE(byte[] data, int offset, long v) {
        int i = offset;
        data[i+2] = (byte)((v >>>  0) & 0xFF);
        data[i+1] = (byte)((v >>>  8) & 0xFF);
        data[i+0] = (byte)((v >>> 16) & 0xFF);
    }

    public static void writeInt4BE(OutputStream out, long v) throws IOException {
        byte[] b4 = new byte[4];
        putInt4BE(b4, 0, v);
        out.write(b4);
    }
    public static void putInt4BE(byte[] data, int offset, long v) {
        int i = offset;
        data[i+3] = (byte)((v >>>  0) & 0xFF);
        data[i+2] = (byte)((v >>>  8) & 0xFF);
        data[i+1] = (byte)((v >>> 16) & 0xFF);
        data[i+0] = (byte)((v >>> 24) & 0xFF);
    }

    /**
     * Gets the integer value that is stored in UTF-8 like fashion, in Big Endian
     *   but with the high bit on each number indicating if it continues or not
     */
    public static long readUE7(InputStream stream) throws IOException {
        int i;
        long v = 0;
        while ((i = stream.read()) >= 0) {
            v = v << 7;
            if ((i & 128) == 128) {
                // Continues
                v += (i&127);
            } else {
                // Last value
                v += i;
                break;
            }
        }
        return v;
    }
//   public static void writeUE7(OutputStream out, long value) throws IOException {
//       // TODO Implement
//   }

    /**
     * @param length The length in BYTES
     */
    public static String getUTF8(byte[] data, int offset, int length) {
        return new String(data, offset, length, UTF8);
    }
    /**
     * Strips off any null padding, if any, from the string
     */
    public static String removeNullPadding(String str) {
        int idx = str.indexOf(0);
        if (idx == -1) {
            return str;
        }
        return str.substring(0, idx);
    }

    /**
     * @return The length in BYTES
     */
    public static int putUTF8(byte[] data, int offset, String str) {
        byte[] s = toUTF8Bytes(str);
        System.arraycopy(s, 0, data, offset, s.length);
        return s.length;
    }
    /**
     * @return The length in BYTES
     */
    public static byte[] toUTF8Bytes(String str) {
        return str.getBytes(UTF8);
    }
    /**
     * Writes the string out as UTF-8
     */
    public static void writeUTF8(OutputStream out, String str) throws IOException {
        byte[] s = str.getBytes(UTF8);
        out.write(s);
    }
    /**
     * Writes out a 4 byte integer of the length (in bytes!) of the
     *  String, followed by the String (as UTF-8)
     */
    public static void writeUTF8WithLength(OutputStream out, String str) throws IOException {
        byte[] s = str.getBytes(UTF8);
        writeInt4(out, s.length);
        out.write(s);
    }

    /**
     * Checks to see if the wanted byte pattern is found in the
     *  within bytes from the given offset
     * @param wanted Byte sequence to look for
     * @param within Bytes to find in
     * @param withinOffset Offset to check from
     */
    public static boolean byteRangeMatches(byte[] wanted, byte[] within, int withinOffset) {
        for (int i=0; i<wanted.length; i++) {
            if (wanted[i] != within[i+withinOffset]) return false;
        }
        return true;
    }

    public static byte[] toByteArray(final InputStream input) throws IOException {
        try {
        	final ByteArrayOutputStream output = new ByteArrayOutputStream();
            copy(input, output);
            return output.toByteArray();
        }catch (IOException e) {
        	throw e;
        }
    }
    
    public static interface ChunkedEvent {
    	public void onData(final byte[] data);
    }
    
    public static void readChunked(final InputStream input, final int chunkSize, final ChunkedEvent handler) throws IOException {
    	byte[] data = new byte[chunkSize];
    	while (input.read(data) != -1) {
    		if (data == null || data.length == 0) {
    			continue;
    		}
    		handler.onData(data);
    	}
    }
}
